import pygame
import threading
import os
import math
import random


class Game:
    _instance_lock = threading.Lock()
    inited = False
    created = False

    def __new__(cls, *args, **kwargs):
        if not cls.created:
            cls.created = True
            with Game._instance_lock:
                if not hasattr(Game, "_instance"):
                    Game._instance = super().__new__(cls)
        return Game._instance

    def __init__(self, screen_width=1280, screen_height=720, full_screen=False):
        pygame.display.set_icon(pygame.image.load(os.path.join("data", "snake_head.png")))
        pygame.display.set_caption("贪吃蛇之吃货的智慧")
        self._is_over = False
        self.score = 0
        if self.inited:
            return
        try:
            pygame.mixer.init()
            pygame.mixer.music.load(os.path.join("data", "mp3.mp3"))
            pygame.mixer.music.play(-1)
        except Exception:
            pass
        Game.inited = True
        pygame.init()
        self.key_down_events = {}
        self.mouse_motions = []
        self.mouse_direction = None
        self._exit = False
        self.clock = pygame.time.Clock()
        args = ((screen_width, screen_height),)
        if full_screen:
            args += (pygame.FULLSCREEN,)
        self.screen = pygame.display.set_mode(*args)
        self.map = Map(screen_width, screen_height)
        self.resources = [self.map]
        self.add_key_down_event_handle(pygame.K_ESCAPE, self.exit)

    def add_key_down_event_handle(self, key, handle):
        if key not in self.key_down_events:
            self.key_down_events[key] = []
        self.key_down_events[key].append(handle)

    def exit(self):
        self._exit = True

    def start(self):
        last_mouse_motion = None
        while not self._exit:
            if self._is_over:
                self.show_over()
                while True:
                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:
                            exit(0)
                    self.clock.tick(1)
            # check key
            ones_response = []
            self.mouse_motions = []
            if last_mouse_motion is not None:
                self.mouse_motions.append(last_mouse_motion)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self._exit = True
                elif event.type == pygame.KEYDOWN and event.key in self.key_down_events:
                    if event.key in ones_response:
                        continue
                    ones_response.append(event.key)
                    for handle in self.key_down_events[event.key]:
                        handle()
                elif event.type == pygame.MOUSEMOTION:
                    # continue
                    self.mouse_motions.append(event)
            if self.mouse_motions:
                last_mouse_motion = self.mouse_motions[-1]
                self._handle_mouse_event()
            # update resources
            for resource in self.resources:
                resource.update(self.screen)
            pygame.display.flip()

            # tick
            self.clock.tick(65)

    def over(self, score):
        self.score = score
        self._is_over = True

    def show_over(self):
        if pygame.font is not None:
            font = pygame.font.SysFont('microsoft Yahei', 70)
            surface = font.render(f"GAME OVER YOU GOT {self.score}.", False, (255, 0, 0))
            self.screen.blit(surface, (200, 200))
        pygame.display.flip()

    def _handle_mouse_event(self):
        precision = 4096
        if len(self.mouse_motions) >= 2:
            move = (self.mouse_motions[0].pos[0] - self.mouse_motions[-1].pos[0],
                    self.mouse_motions[0].pos[1] - self.mouse_motions[-1].pos[1])
            sq = (move[0] ** 2 + move[1] ** 2) ** (1 / 2)
            self.mouse_direction = (-1 * move[0] / sq, move[1] / sq)
            self.mouse_direction = [int(i * precision) / precision for i in self.mouse_direction]
        else:
            self.mouse_direction = None


class Resource:
    game = None

    @classmethod
    def _get_game(cls):
        cls.game = Game()

    @classmethod
    def create_surface(cls, width, height, image=None, color=(255, 255, 0), init_angle=0):
        if image is not None:
            surface = pygame.image.load(os.path.join("data", image))
        else:
            surface = pygame.Surface((width, height))
            surface.fill(color)
        surface = pygame.transform.scale(surface, (width, height))
        surface = pygame.transform.rotate(surface, init_angle).convert_alpha()
        return surface

    def __init__(self):
        self._get_game()

    def update(self, dist):
        pass


class MetaResource(Resource):
    def __init__(self, cx=0.0, cy=0.0, width=0.0, height=0.0, surface=None, color=(0, 0, 255)):
        super().__init__()
        self.width = width
        self.height = height
        self.color = color
        if surface is None:
            self.surface = self.create_surface(width, height, color=color)
            self.redraw()
        else:
            self.surface = surface
        self.cx = cx
        self.cy = cy

    @property
    def surface(self):
        return self._surface

    @surface.setter
    def surface(self, value):
        self.width = value.get_width()
        self.height = value.get_height()
        self._surface = value
        self._surface_width_d2 = self.width / 2
        self._surface_height_d2 = self.height / 2

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

    @property
    def cx(self):
        return self._cx

    @property
    def cy(self):
        return self._cy

    @x.setter
    def x(self, value):
        if value < 0 or value > 1280:
            raise Exception(f"X{value}")
        self._x = value
        self._cx = value + self._surface_width_d2

    @y.setter
    def y(self, value):
        if value < 0 or value > 720:
            raise Exception(f"Y{value}")
        self._y = value
        self._cy = value + self._surface_height_d2

    @cx.setter
    def cx(self, value):
        self._cx = value
        self._x = value - self._surface_width_d2

    @cy.setter
    def cy(self, value):
        self._cy = value
        self._y = value - self._surface_height_d2

    def redraw(self):
        self.surface.fill(self.color)

    def update(self, dist):
        dist.blit(self.surface, (self.x, self.y),
                  (self.x, self.y, self.x + self.width, self.y + self.height))

    def this_position_is_in_me(self, position):
        x, y = position
        if self.x < x < self.x + self.width and self.y < y < self.y + self.height:
            return True
        return False


class Node(MetaResource):
    def __init__(self, parent, surface, cx=0, cy=0, direction=(0, 1)):
        super().__init__(cx=cx, cy=cy, surface=surface)
        self._fist_surface = surface
        self.angel = 0
        self.prev = None
        self.parent = parent
        self._t_direction = direction
        self.direction_change_queue = []
        self._change_direction = self._t_direction
        self._change_position = (cx, cy)
        self._changing = False

    def cal_angel(self):
        angel = math.degrees(math.asin(self._change_direction[1]))
        if self._change_direction[0] <= 0 and self._change_direction[1] <= 0:
            angel = -180 - angel
        elif self._change_direction[0] <= 0 and 0 <= self._change_direction[1]:
            angel = 180 - angel
        diff = math.fabs(self.angel - angel)
        self.angel = angel
        if diff >= 360:
            diff -= 360
        if diff > 180:
            diff -= 180
        return diff

    def rotate(self):
        self.surface = pygame.transform.rotate(self._fist_surface, self.angel)
        x, y = self.cx, self.cy
        self.cx, self.cy = x, y

    @property
    def direction(self):
        return self._t_direction

    def change_surface(self, surface, index):
        diff_size = (self._fist_surface.get_width() - surface.get_width())
        diff_size *= index
        self._fist_surface = surface
        self.cx, self.cy = self.cx + self.direction[0] * diff_size, self.cy - self.direction[1] * diff_size
        self.rotate()

    def update(self, dist):
        dist.blit(self.surface, (self.x, self.y))

    def calculate_next_step(self, speed):
        res = (self.cx + self.direction[0] * speed, self.cy + self.direction[1] * speed * -1)
        return res

    def _move(self, speed):
        self.cx, self.cy = self.calculate_next_step(speed)

    def move(self):
        if not self._changing and len(self.prev.direction_change_queue) > 0:
            self._change_position, self._change_direction = self.prev.direction_change_queue.pop()
            if self.direction != self._change_direction:
                self._changing = True

        if self._changing:
            x, y = self.calculate_next_step(self.parent.speed)
            y = -y
            equ = [0, 0]
            remain_speed_x = 0
            remain_speed_y = 0
            if self.direction[0] > 1.0e-8:
                if x > self._change_position[0]:
                    remain_speed_x = x - self._change_position[0]
                    remain_speed_x /= self.direction[0]
                elif x == self._change_position[0]:
                    equ[0] = 1
            elif self.direction[0] < -1.0e-8:
                if x < self._change_position[0]:
                    remain_speed_x = self._change_position[0] - x
                    remain_speed_x /= (self.direction[0] * -1)
                elif x == self._change_position[0]:
                    equ[0] = 1

            if self.direction[1] > 1.0e-8:
                if y > self._change_position[1]:
                    remain_speed_y = y - self._change_position[1]
                    remain_speed_y /= self.direction[1]
                elif y == self._change_position[1]:
                    equ[1] = 1
            elif self.direction[1] < -1.0e-8:
                if y < self._change_position[1]:
                    remain_speed_y = self._change_position[1] - y
                    remain_speed_y /= (self.direction[1] * -1)
                elif y == self._change_position[1]:
                    equ[1] = 1

            remain_speed = max(remain_speed_x, remain_speed_y)
            speed = self.parent.speed - remain_speed
            self._move(speed)
            if remain_speed > 0 or (equ[0] and equ[1]):
                angel = self.cal_angel()
                self.rotate()
                self._t_direction = self._change_direction
                self._changing = False
                if self.parent.body[-1] is not self:
                    self.direction_change_queue.insert(0, ((self.cx, -self.cy), self.direction))
                self._move(remain_speed)

        else:
            self._move(self.parent.speed)


class NodeHead(Node):

    @property
    def direction(self):
        return self._t_direction

    @direction.setter
    def direction(self, value):
        self._change_direction = value

    def move(self):
        if self.game.mouse_direction is not None:
            self.direction = self.game.mouse_direction
        angle = self.cal_angel()
        if 5 < angle < 175:
            self._t_direction = self._change_direction
            self.direction_change_queue.insert(0, ((self.cx, -self.cy), self.direction))
            self.rotate()
        self._move(self.parent.speed)


class Creature(Resource):
    fat = 100
    sugar = 100
    health = 100
    node_size = 20

    def __init__(self, init_x=400, init_y=100):
        super().__init__()
        self.init_x = init_x
        self.init_y = init_y
        self.body = []

    def check_the_position_is_token(self, position, just_check_head=True):
        if just_check_head:
            checks = [self.body[0]]
        else:
            checks = self.body
        for c in checks:
            res = c.this_position_is_in_me(position)
            if res:
                return res
        return False

    def move(self):
        pass

    def update(self, dist):
        self.move()
        for i in range(len(self.body) - 1, -1, -1):
            self.body[i].update(dist)

    @property
    def cx(self):
        return self.body[0].cx

    @property
    def cy(self):
        return self.body[0].cy


class Snake(Creature):
    body_img = "snake_body.png"
    head_img = "snake_head.png"

    def __init__(self, node_size=20, init_x=400, init_y=100, health=200,
                 hungry=200, direction=(1, 0), init_len=3):
        super().__init__(init_x, init_y)
        self.health = health
        self.hungry = hungry
        self.speed = 1
        self.score = 0
        self._node_size = node_size
        self._eat_sugar = 0
        self._eat_fat = 0
        self.head_surface = None
        self.body_surface = None
        self.create_body()
        # add event
        self.game.add_key_down_event_handle(pygame.K_DOWN, self.down)
        self.game.add_key_down_event_handle(pygame.K_UP, self.up)
        self.game.add_key_down_event_handle(pygame.K_RIGHT, self.right)
        self.game.add_key_down_event_handle(pygame.K_LEFT, self.left)
        self.body = [NodeHead(self, self.head_surface, self.init_x, self.init_y, direction)]
        for i in range(init_len - 1):
            self.init_grow()

    def create_body(self):
        self.body_surface = self.create_surface(self.node_size, self.node_size, self.body_img, init_angle=270)
        self.head_surface = self.create_surface(self.node_size, self.node_size, self.head_img, init_angle=270)

    def eat(self, f):
        self._eat_sugar += f.sugar
        self._eat_fat += f.fat
        if f.health < f.corrosion_h:
            self.health -= 10
            self.score += 10
        else:
            self.health += 5
            self.score += 20
            if self.health > 200:
                self.health = 200
        self.hungry += f.sugar
        if self.hungry > 200:
            self.hungry = 200

    @property
    def speed(self):
        return self._speed

    @speed.setter
    def speed(self, speed):
        if speed <= 0:
            speed = 0.2
        speed = int(speed * 128) / 128
        self._speed = speed

    @property
    def node_size(self):
        return self._node_size

    @node_size.setter
    def node_size(self, value):
        self.speed = self.speed - (value - self.node_size) / 4
        self._node_size = value
        self.create_body()
        self.body[0].change_surface(self.head_surface, 0)
        for i in range(1, len(self.body)):
            self.body[i].change_surface(self.body_surface, i)

    def update_data(self):
        self.hungry -= 0.1
        if self.hungry < 0:
            self.hungry = 0
            if self.node_size > 10:
                self.node_size -= 1
            elif len(self.body) > 2:
                self.body.pop()
            else:
                self.health = 0
            self.hungry += 50
            self.health -= 10

        if self._eat_fat > 100:
            self._eat_fat -= 100
            self.node_size += 2
        if self._eat_sugar > 100:
            self._eat_sugar -= 100
            self.grow()

    def move(self):
        self.update_data()
        for s in self.body:
            s.move()

    def init_grow(self):
        last_node = self.body[-1]
        d = last_node.direction
        node = Node(self, self.body_surface, last_node.cx - d[0] * self.node_size,
                    last_node.cy - d[1] * self.node_size, d)
        node.angel = last_node.angel
        node.rotate()
        node.prev = last_node
        self.body.append(node)

    def grow(self):
        self.speed += 0.1
        last_node = self.body[-1]
        d = last_node.direction
        node = Node(self, self.body_surface, last_node.cx - d[0] * self.node_size,
                    last_node.cy + d[1] * self.node_size, d)
        node.angel = last_node.angel
        node.rotate()
        node.prev = last_node
        self.body.append(node)

    def check_eat_self(self):
        for s in self.body[1:]:
            if s.this_position_is_in_me((self.body[0].cx, self.body[0].cy)):
                return True
        return False

    def down(self):
        self.body[0].direction = (0, -1)

    def up(self):
        self.hungry -= 0.1
        self.body[0].direction = (0, 1)

    def right(self):
        self.body[0].direction = (1, 0)

    def left(self):
        self.body[0].direction = (-1, 0)


class Dog(Creature):
    head_img = "wangchai.png"
    node_size = 40

    def __init__(self, init_x=400, init_y=100, health=20000,
                 hungry=20000):
        super().__init__(init_x, init_y)
        self.health = health
        self.hungry = hungry
        self.speed = 1
        self.head_surface = None
        self.create_body()
        self.body = [NodeHead(self, self.head_surface, self.init_x, self.init_y, (0, 1))]

    def move(self):
        self.health -= 1
        pass

    def create_body(self):
        self.head_surface = self.create_surface(self.node_size, self.node_size, self.head_img)


class Food(Creature):
    health = 800
    corrosion_h = 500
    image_path = "food.png"
    corrosion_rate = 1
    flashing = 8

    def __init__(self, init_x=100, init_y=100):
        super().__init__(init_x, init_y)
        surface = self.create_surface(self.node_size, self.node_size, self.image_path)
        self.body.append(NodeHead(self, surface, self.init_x, self.init_y))
        self._time = 0

    def corrosion(self):
        self.health -= self.corrosion_rate

    def update(self, dist):
        self.corrosion()
        if self.health < self.corrosion_h:
            self._time += 1
            if self._time >= self.flashing:
                self._time = 0
                return
        super().update(dist)


class Apple(Food):
    fat = 0
    sugar = 100
    node_size = 50
    corrosion_rate = 1
    image_path = "apple.png"


class Banana(Food):
    fat = 0
    sugar = 80
    node_size = 30
    corrosion_rate = 1
    image_path = "banana.png"


class Grape(Food):
    fat = 0
    sugar = 50
    node_size = 30
    corrosion_rate = 1
    image_path = "grape.png"


class Hamburg(Food):
    fat = 50
    sugar = 150
    node_size = 40
    corrosion_rate = 1
    image_path = "hamburg.png"


class Agg(Food):
    fat = 50
    sugar = 100
    node_size = 30
    corrosion_rate = 1
    image_path = "agg.png"


class Chicken(Food):
    fat = 100
    sugar = 150
    node_size = 60
    corrosion_rate = 1
    image_path = "chicken.png"


class Map(MetaResource):
    foods = [Agg, Apple, Chicken, Grape, Banana, Hamburg]

    def __init__(self, width, height):
        super().__init__(width=width, height=height, cx=width / 2, cy=height / 2)
        self.resources = []
        self.r_foods = []
        self.dogs = []
        self.player = Snake()
        if pygame.font is not None:
            self.font = pygame.font.SysFont('microsoft Yahei', 30)
        self.resources.append(self.player)

    def show_player_state(self):
        y = 0
        surface = self.font.render('score: %3.1f' % self.player.score, False, (40, 0, 10))
        self.surface.blit(surface, (0, y))
        y += surface.get_height() + 10
        surface = self.font.render('health: %3.1f' % self.player.health, False, (255, 100, 10))
        self.surface.blit(surface, (0, y))
        y += surface.get_height() + 10
        surface = self.font.render('hunger: %3.1f' % self.player.hungry, False, (255, 100, 10))
        self.surface.blit(surface, (0, y))

    def update(self, dist):
        # add food
        if len(self.r_foods) < 3:
            f = random.choice(self.foods)
            pos = self.rand_position(f)
            if pos is not None:
                self.r_foods.append(f(*pos))
        # add dogs
        if len(self.dogs) < 4:
            pos = self.rand_position(Dog)
            if pos is not None:
                self.dogs.append(Dog(*pos))
        # check foods:
        rm = []
        for f in self.r_foods:
            if f.health < 0:
                rm.append(f)
        for f in rm:
            self.r_foods.remove(f)

        # check player
        if not self.this_position_is_in_me((self.player.cx, self.player.cy)):
            self.player.health = -10
        # if self.player.check_eat_self(): allow body overlap
        #    self.player.health = -10
        for d in self.dogs:
            if d.check_the_position_is_token((self.player.cx, self.player.cy)):
                self.player.health = -10
                break
        if self.player.health < -1:
            self.game.over(self.player.score)
        for f in self.r_foods:
            if self.player.check_the_position_is_token((f.cx, f.cy)) \
                    or f.check_the_position_is_token((self.player.cx, self.player.cy)):
                self.player.eat(f)
                self.r_foods.remove(f)
                break

        self.redraw()
        for s in self.resources + self.r_foods + self.dogs:
            s.update(self.surface)
        self.show_player_state()
        super().update(dist)

    def rand_position(self, item):
        interval = item.node_size + 10
        for i in range(100):
            p = random.randint(interval, self.width - 2 * interval), \
                random.randint(interval, self.height - 2 * interval)
            if not self.check_the_position_is_token(p):
                return p
        return None

    def check_the_position_is_token(self, position, just_check_head=True):
        for s in self.resources + self.r_foods:
            res = s.check_the_position_is_token(position, just_check_head=just_check_head)
            if res:
                return res
        return False


if __name__ == "__main__":
    game = Game(full_screen=False)
    game.start()
